Skip to content

feat(configurator): rework Color Studio panels and add per-color ShadeRamp metrics - #409

Closed
jackgranatowski wants to merge 1 commit into
mainfrom
codex/add-active-panel-and-color-states-to-colorstudio
Closed

feat(configurator): rework Color Studio panels and add per-color ShadeRamp metrics#409
jackgranatowski wants to merge 1 commit into
mainfrom
codex/add-active-panel-and-color-states-to-colorstudio

Conversation

@jackgranatowski

Copy link
Copy Markdown
Contributor

Motivation

  • Provide a focused, studio-first UI for colors by exposing explicit panels for Main colors, Semantic colors, Gradients, Shade curve, Contrast and Assignments.
  • Surface the per-color controls and ramp info in a compact Automatic.css-style accordion so designers can inspect and edit each brand source in context.
  • Consolidate duplicate smart settings (palette curve / gradient builder) into the visual Color Studio to avoid having the same controls shown twice.

Description

  • Added panel navigation state to ColorStudio.svelte with activePanel = 'main-colors' and per-color selection activeColor = 'primary' and implemented the left/top studio nav listing the requested panels.
  • Implemented an Automatic.css-inspired main color accordion in ColorStudio.svelte that lists Primary, Secondary, Tertiary, Accent/Action, Base, Neutral with toggle/chevron, swatch and expandable per-color content that mounts BrandColorRow and ShadeRamp.
  • Moved the gradient builder and palette shade-curve views into the Gradients and Shade curve panels respectively by reading smartSettingsFor and rendering those controls inside ColorStudio.svelte.
  • Extended ShadeRamp.svelte to accept colorKey and showIntro props, support rendering a single color family, and add a Shade / Lightness / Chroma table computed from measured swatch colors.
  • Moved ColorAssignments into the Assignments panel and left only the All variables disclosure below studios by suppressing SmartSettings for the colors and gradients domains in DomainPanel.svelte.
  • Removed an unused import and added a few helper bindings (stageStyle, paletteCurve, gradientBuilder, token helpers) and supporting styles for the new UI pieces.

Testing

  • Ran npm --prefix configurator run check (Svelte diagnostics) which completed successfully with the existing, non-blocking warnings reported.
  • Ran npm --prefix configurator run build (Vite production build) which completed successfully though the build emitted the existing Svelte deprecation/state-capture and chunk-size warnings.
  • Attempted UI screenshot via Playwright, but browser installation failed in this environment due to a CDN 403 preventing Playwright from downloading browsers, so the visual snapshot step did not complete.

Codex Task

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@jackgranatowski, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 28 minutes and 52 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9ec82efd-eb53-4f2e-b22b-6cc22cd80cfa

📥 Commits

Reviewing files that changed from the base of the PR and between f391ec2 and b279ee6.

📒 Files selected for processing (3)
  • configurator/src/components/DomainPanel.svelte
  • configurator/src/components/ShadeRamp.svelte
  • configurator/src/components/editors/ColorStudio.svelte
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/add-active-panel-and-color-states-to-colorstudio

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Rework Color Studio into panel-based UI with per-color ShadeRamp metrics
✨ Enhancement 🕐 40+ Minutes

Grey Divider

Description

• Add panel navigation and per-color accordion state to Color Studio.
• Embed gradient builder and palette curve controls directly inside Color Studio panels.
• Extend ShadeRamp to support per-color display and Lightness/Chroma metrics table.
Diagram

graph TD
  D((Designer)) --> CS["ColorStudio.svelte"] --> OS[("Overrides + UI state")]
  CS --> CC["Brand/role controls"]
  CS --> SR["ShadeRamp.svelte"]
  CS --> DS["smartSettingsFor()"]
  DP["DomainPanel.svelte"] -. "skip colors/gradients" .-> SS["SmartSettings"]

  subgraph Legend
    direction LR
    _u((User)) ~~~ _c["Component"] ~~~ _s[("State/store")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Extract each panel into its own Svelte component
  • ➕ Reduces ColorStudio.svelte size and cognitive load
  • ➕ Enables panel-level unit/UI testing and reuse
  • ➕ Simplifies future additions (new panels, lazy loading)
  • ➖ More files/boilerplate and prop plumbing for shared state (activeColor, overrides)
  • ➖ May require a small shared store or context to avoid prop drilling
2. Persist activePanel/activeColor in URL or shared store
  • ➕ Deep-linkable panels/colors for design review workflows
  • ➕ Preserves state across reloads and navigation
  • ➖ Adds routing/state sync complexity
  • ➖ Needs decisions about defaulting/migration of old behavior

Recommendation: Current approach is reasonable for quickly consolidating duplicated smart settings into the Color Studio. If this UI is expected to keep growing, the next step should be extracting each panel into dedicated components and optionally persisting panel/color selection (URL or shared store) to keep ColorStudio.svelte from becoming a long-lived monolith.

Files changed (3) +175 / -70

Enhancement (2) +174 / -69
ShadeRamp.svelteAdd per-color filtering and Shade/Lightness/Chroma metrics table +38/-6

Add per-color filtering and Shade/Lightness/Chroma metrics table

• Introduces colorKey/showIntro props to optionally render a single brand color family and hide intro/note text. Adds computed shade metrics (lightness percentage + chroma proxy) and renders them in a compact table per ramp, plus associated table styles.

configurator/src/components/ShadeRamp.svelte

ColorStudio.svelteRebuild Color Studio as panel-based UI with per-color accordion and embedded smart controls +136/-63

Rebuild Color Studio as panel-based UI with per-color accordion and embedded smart controls

• Adds explicit panel navigation state (main colors, semantic colors, gradients, shade curve, contrast, assignments) and a main-color accordion with per-color expansion and selection. Moves gradient builder and palette curve smart settings into their respective panels (including presets and sliders) and relocates assignments into a dedicated panel, with new helper bindings and styling to support the redesigned layout.

configurator/src/components/editors/ColorStudio.svelte

Refactor (1) +1 / -1
DomainPanel.svelteSuppress SmartSettings for colors/gradients domains +1/-1

Suppress SmartSettings for colors/gradients domains

• Prevents rendering SmartSettings when the active domain is 'colors' or 'gradients'. This avoids duplicated controls now that those smart sections are embedded in Color Studio panels.

configurator/src/components/DomainPanel.svelte

@qodo-code-review

Copy link
Copy Markdown

CI Feedback 🧐

A test triggered by this PR failed. Here is an AI-generated analysis of the failure:

Action: Configurator tests

Failed stage: Run npm test [❌]

Failed test name: visual studios > Color Studio mounts with preview and curated controls

Failure summary:

The action failed because a Vitest component test failed in tests-components/studios.test.js.
-
Failing test: visual studios > Color Studio mounts with preview and curated controls
- Error:
TestingLibraryElementError — the test expected to find UI text Source pairs, but no element with
that text was rendered (or the text is split across multiple elements, causing getAllByText to not
match).
- Location: tests-components/studios.test.js:43:16, where the assertion
expect(getAllByText(text).length).toBeGreaterThan(0); failed.
This caused vitest run to exit with
code 1, failing the GitHub Action.

Relevant error logs:
1:  ##[group]Runner Image Provisioner
2:  Hosted Compute Agent
...

1091:  type: 'test'
1092:  ...
1093:  1..3
1094:  ok 28 - bestContrastVsBW
1095:  ---
1096:  duration_ms: 0.829151
1097:  type: 'suite'
1098:  ...
1099:  # Subtest: wcagLevel
1100:  # Subtest: classifies known thresholds
1101:  ok 1 - classifies known thresholds
1102:  ---
1103:  duration_ms: 2.161159
1104:  type: 'test'
1105:  ...
1106:  # Subtest: non-finite ratios fail
1107:  ok 2 - non-finite ratios fail
1108:  ---
...

3960:  ok 7 - storage key is versioned
3961:  ---
3962:  duration_ms: 0.321708
3963:  type: 'test'
3964:  ...
3965:  1..7
3966:  ok 88 - sanitiseUiState
3967:  ---
3968:  duration_ms: 5.729428
3969:  type: 'suite'
3970:  ...
3971:  1..88
3972:  # tests 525
3973:  # suites 88
3974:  # pass 525
3975:  # fail 0
3976:  # cancelled 0
3977:  # skipped 0
3978:  # todo 0
3979:  # duration_ms 1063.183167
3980:  > slashed-configurator@0.6.15 test:components
3981:  > vitest run
3982:  �[1m�[46m RUN �[49m�[22m �[36mv3.2.6 �[39m�[90m/home/runner/work/SLASHED/SLASHED/configurator�[39m
3983:  �[32m✓�[39m tests-components/icon.test.js �[2m(�[22m�[2m5 tests�[22m�[2m)�[22m�[32m 38�[2mms�[22m�[39m
3984:  9:51:53 AM [vite-plugin-svelte] src/components/ControlSection.svelte:3:47 This reference only captures the initial value of `defaultOpen`. Did you mean to reference it inside a derived instead?
3985:  https://svelte.dev/e/state_referenced_locally
3986:  �[32m✓�[39m tests-components/domain-preview.test.js �[2m(�[22m�[2m12 tests�[22m�[2m)�[22m�[33m 999�[2mms�[22m�[39m
3987:  �[32m✓�[39m tests-components/bundle-picker.test.js �[2m(�[22m�[2m4 tests�[22m�[2m)�[22m�[32m 236�[2mms�[22m�[39m
3988:  �[32m✓�[39m tests-components/shade-ramp.test.js �[2m(�[22m�[2m2 tests�[22m�[2m)�[22m�[33m 594�[2mms�[22m�[39m
3989:  �[33m�[2m✓�[22m�[39m ShadeRamp�[2m > �[22mrenders the six brand rows with seven swatches each �[33m 345�[2mms�[22m�[39m
3990:  �[32m✓�[39m tests-components/share-button.test.js �[2m(�[22m�[2m2 tests�[22m�[2m)�[22m�[32m 71�[2mms�[22m�[39m
3991:  �[31m❯�[39m tests-components/studios.test.js �[2m(�[22m�[2m10 tests�[22m�[2m | �[22m�[31m1 failed�[39m�[2m)�[22m�[33m 7904�[2mms�[22m�[39m
3992:  �[33m�[2m✓�[22m�[39m visual studios�[2m > �[22mTypography Studio mounts with preview and curated controls �[33m 828�[2mms�[22m�[39m
...

3994:  �[31m     → Unable to find an element with the text: Source pairs. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
3995:  Ignored nodes: comments, script, style
3996:  �[36m<body>�[31m
3997:  �[36m<div>�[31m
3998:  �[36m<section�[31m
3999:  �[33mclass�[31m=�[32m"studio studio--color svelte-10uh662"�[31m
4000:  �[33mstyle�[31m=�[32m"color-scheme: light; --sf-is-dark: 0; --sf-alternate-gap: var(--sf-content-gap); --sf-alternate-inner-gap: var(--sf-gap); --sf-animation-blink: sf-blink calc(1s * var(--sf-motion-scale)) steps(1, end) infinite; --sf-animation-color-pulse: sf-color-pulse var(--sf-duration-slow) var(--sf-ease-in-out) infinite; --sf-animation-delay-1: calc(75ms * var(--sf-motion-scale)); --sf-animation-delay-2: calc(150ms * var(--sf-motion-scale)); --sf-animation-delay-3: calc(225ms * var(--sf-motion-scale)); --sf-animation-delay-4: calc(300ms * var(--sf-motion-scale)); --sf-animation-delay-5: calc(375ms * var(--sf-motion-scale)); --sf-animation-fade-in: sf-fade-in var(--sf-duration-normal) var(--sf-ease-out) both; --sf-animation-fade-out: sf-fade-out var(--sf-duration-normal) var(--sf-ease-in) both; --sf-animation-float: sf-float calc(3s * var(--sf-motion-scale)) var(--sf-ease-in-out) infinite; --sf-animation-ping: sf-ping var(--sf-duration-slow) var(--sf-ease-out) infinite; --sf-animation-scale-down: sf-scale-down var(--sf-duration-normal) var(--sf-ease-in) both; --sf-animation-scale-up: sf-scale-up var(--sf-duration-normal) var(--sf-ease-overshoot) both; --sf-animation-shimmer: sf-shimmer calc(1.5s * var(--sf-motion-scale)) var(--sf-ease-in-out) infinite; --sf-animation-slide-in-down: sf-slide-in-down var(--sf-duration-normal) var(--sf-ease-out) both; --sf-animation-slide-in-left: sf-slide-in-left var(--sf-duration-normal) var(--sf-ease-out) both; --sf-animation-slide-in-right: sf-slide-in-right var(--sf-duration-normal) var(--sf-ease-out) both; --sf-animation-slide-in-up: sf-slide-in-up var(--sf-duration-normal) var(--sf-ease-out) both; --sf-animation-spin: sf-spin var(--sf-duration-slower) linear infinite; --sf-aspect: 16 / 9; --sf-bento-cols-default: 4; --sf-bento-gap: var(--sf-gap); --sf-bento-row-compact: 6rem; --sf-bento-row-default: 10rem; --sf-bento-row-tall: 16rem; --sf-blur: 12px; --sf-body-color: var(--sf-color-text); --sf-body-em-style: italic; --sf-body-font-family: var(--sf-font-body); --sf-body-font-size: var(--sf-text-m); --sf-body-font-weight: var(--sf-font-weight-body); --sf-body-line-height: var(--sf-leading-normal); --sf-body-strong-weight: var(--sf-font-weight-strong); --sf-body-text-wrap: pretty; --sf-border: var(--sf-border-width-1) var(--sf-border-style) var(--sf-color-border); --sf-border-scale: 1; --sf-border-strong: var(--sf-border-width-1) var(--sf-border-style) var(--sf-color-border--strong); --sf-border-style: solid; --sf-border-subtle: var(--sf-border-width-1) var(--sf-border-style) var(--sf-color-border--subtle); --sf-border-width-1: calc(1px * var(--sf-border-scale, 1)); --sf-border-width-2: calc(2px * var(--sf-border-scale, 1)); --sf-border-width-3: calc(3px * var(--sf-border-scale, 1)); --sf-border-width-4: calc(4px * var(--sf-border-scale, 1)); --sf-border-width-hairline: 0.5px; --sf-box-border-color: var(--sf-color-border); --sf-box-border-width: 0; --sf-box-padding: var(--sf-space-m); --sf-breakout-width: var(--sf-container-wide); --sf-button-padding-block: var(--sf-space-xs); --sf-button-padding-inline: var(--sf-space-m); --sf-button-radius: var(--sf-radius-m); --sf-caret-color: var(--sf-color-action); --sf-center-gutter: var(--sf-gutter); --sf-center-max: var(--sf-container-default); --sf-cluster-align: center; --sf-cluster-gap: var(--sf-gap); --sf-cluster-justify: flex-start; --sf-code-font-size: 0.875em; --sf-color-action: var(--sf-color-action-source-light); --sf-color-action--active: var(--sf-color-action-xdark); --sf-color-action--hover: var(--sf-color-action-darker); --sf-color-action-100: color-mix(in oklab, var(--sf-color-action) var(--sf-palette-mix-100), var(--sf-color-surface)); --sf-color-action-200: color-mix(in oklab, var(--sf-color-action) var(--sf-palette-mix-200), var(--sf-color-surface)); --sf-color-action-300: color-mix(in oklab, var(--sf-color-action) var(--sf-palette-mix-300), var(--sf-color-surface)); --sf-color-action-400: color-mix(in oklab, var(--sf-color-action) var(--sf-palette-mix-400), var(--sf-color-surface)); --sf-color-action-50: color-mix(in oklab, var(--sf-color-action) var(--sf-palette-mix-50), var(--sf-color-surface)); --sf-color-action-500: var(--sf-color-action); --sf-color-action-600: color-mix(in oklab, var(--sf-color-action) var(--sf-palette-mix-600), var(--sf-color-text)); --sf-color-action-700: color-mix(in oklab, var(--sf-color-action) var(--sf-palette-mix-700), var(--sf-color-text)); --sf-color-action-800: color-mix(in oklab, var(--sf-color-action) var(--sf-palette-mix-800), var(--sf-color-text)); --sf-color-action-900: color-mix(in oklab, var(--sf-color-action) var(--sf-palette-mix-900), var(--sf-color-text)); --sf-color-action-950: color-mix(in oklab, var(--sf-color-action) var(--sf-palette-mix-950), var(--sf-color-text)); --sf-color-action-a10: oklch(from var(--sf-color-action) l c h / 0.10); --sf-color-action-a30: oklch(from var(--sf-color-action) l c h / 0.30); --sf-color-action-a5: oklch(from var(--sf-color-action) l c h / 0.05); --sf-color-action-a50: oklch(from var(--sf-color-action) l c h / 0.50); --sf-color-action-a80: oklch(from var(--sf-color-action) l c h / 0.80); --sf-color-action-darker: var(--sf-color-action-600); --sf-color-action-ghost: oklch(from var(--sf-color-action) l c h / 0.05); --sf-color-action-lighter: var(--sf-color-action-400); --sf-color-action-muted: oklch(from var(--sf-color-action) l c h / 0.30); --sf-color-action-source-dark: oklch(0.70 0.198 235); --sf-color-action-source-light: oklch(0.50 0.22 235); --sf-color-action-subtle: oklch(from var(--sf-color-action) l c h / 0.10); --sf-color-action-superdark: var(--sf-color-action-950); --sf-color-action-superlight: var(--sf-color-action-50); --sf-color-action-xdark: var(--sf-color-action-800); --sf-color-action-xlight: var(--sf-color-action-200); --sf-color-base: var(--sf-color-base-source-light); --sf-color-base--active: var(--sf-color-base-xdark); --sf-color-base--hover: var(--sf-color-base-darker); --sf-color-base-100: color-mix(in oklab, var(--sf-color-text) var(--sf-palette-mix-100), var(--sf-color-base)); --sf-color-base-200: color-mix(in oklab, var(--sf-color-text) var(--sf-palette-mix-200), var(--sf-color-base)); --sf-color-base-300: color-mix(in oklab, var(--sf-color-text) var(--sf-palette-mix-300), var(--sf-color-base)); --sf-color-base-400: color-mix(in oklab, var(--sf-color-text) var(--sf-palette-mix-400), var(--sf-color-base)); --sf-color-base-50: color-mix(in oklab, var(--sf-color-text) var(--sf-palette-mix-50), var(--sf-color-base)); --sf-color-base-500: var(--sf-color-base); --sf-color-base-600: color-mix(in oklab, var(--sf-color-base) var(--sf-palette-mix-600), var(--sf-color-text)); --sf-color-base-700: color-mix(in oklab, var(--sf-color-base) var(--sf-palette-mix-700), var(--sf-color-text)); --sf-color-base-800: color-mix(in oklab, var(--sf-color-base...�[39m
4001:  �[33m�[2m✓�[22m�[39m visual studios�[2m > �[22mSpacing Studio mounts with preview and curated controls �[33m 985�[2mms�[22m�[39m
4002:  �[33m�[2m✓�[22m�[39m visual studios�[2m > �[22mLayout Studio mounts with preview and curated controls �[33m 904�[2mms�[22m�[39m
4003:  �[33m�[2m✓�[22m�[39m visual studios�[2m > �[22mShape Studio mounts with preview and curated controls �[33m 1065�[2mms�[22m�[39m
4004:  �[33m�[2m✓�[22m�[39m visual studios�[2m > �[22mShadow Studio mounts with preview and curated controls �[33m 993�[2mms�[22m�[39m
4005:  �[33m�[2m✓�[22m�[39m visual studios�[2m > �[22mMotion Studio mounts with preview and curated controls �[33m 1117�[2mms�[22m�[39m
4006:  �[33m�[2m✓�[22m�[39m visual studios�[2m > �[22mEffects Studio mounts with preview and curated controls �[33m 580�[2mms�[22m�[39m
4007:  �[33m�[2m✓�[22m�[39m visual studios�[2m > �[22mTypography Studio switches scopes and renders active panel controls �[33m 941�[2mms�[22m�[39m
4008:  �[32m✓�[39m visual studios�[2m > �[22mFriendlyControl renders schema select controls and writes overrides�[32m 122�[2mms�[22m�[39m
4009:  �[31m⎯⎯⎯⎯⎯⎯⎯�[39m�[1m�[41m Failed Tests 1 �[49m�[22m�[31m⎯⎯⎯⎯⎯⎯⎯�[39m
4010:  �[41m�[1m FAIL �[22m�[49m tests-components/studios.test.js�[2m > �[22mvisual studios�[2m > �[22mColor Studio mounts with preview and curated controls
4011:  �[31m�[1mTestingLibraryElementError�[22m�[39m: Unable to find an element with the text: Source pairs. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
4012:  Ignored nodes: comments, script, style
4013:  �[36m<body>�[39m
4014:  �[36m<div>�[39m
4015:  �[36m<section�[39m
4016:  �[33mclass�[39m=�[32m"studio studio--color svelte-10uh662"�[39m
4017:  �[33mstyle�[39m=�[32m"color-scheme: light; --sf-is-dark: 0; --sf-alternate-gap: var(--sf-content-gap); --sf-alternate-inner-gap: var(--sf-gap); --sf-animation-blink: sf-blink calc(1s * var(--sf-motion-scale)) steps(1, end) infinite; --sf-animation-color-pulse: sf-color-pulse var(--sf-duration-slow) var(--sf-ease-in-out) infinite; --sf-animation-delay-1: calc(75ms * var(--sf-motion-scale)); --sf-animation-delay-2: calc(150ms * var(--sf-motion-scale)); --sf-animation-delay-3: calc(225ms * var(--sf-motion-scale)); --sf-animation-delay-4: calc(300ms * var(--sf-motion-scale)); --sf-animation-delay-5: calc(375ms * var(--sf-motion-scale)); --sf-animation-fade-in: sf-fade-in var(--sf-duration-normal) var(--sf-ease-out) both; --sf-animation-fade-out: sf-fade-out var(--sf-duration-normal) var(--sf-ease-in) both; --sf-animation-float: sf-float calc(3s * var(--sf-motion-scale)) var(--sf-ease-in-out) infinite; --sf-animation-ping: sf-ping var(--sf-duration-slow) var(--sf-ease-out) infinite; --sf-animation-scale-down: sf-scale-down var(--sf-duration-normal) var(--sf-ease-in) both; --sf-animation-scale-up: sf-scale-up var(--sf-duration-normal) var(--sf-ease-overshoot) both; --sf-animation-shimmer: sf-shimmer calc(1.5s * var(--sf-motion-scale)) var(--sf-ease-in-out) infinite; --sf-animation-slide-in-down: sf-slide-in-down var(--sf-duration-normal) var(--sf-ease-out) both; --sf-animation-slide-in-left: sf-slide-in-left var(--sf-duration-normal) var(--sf-ease-out) both; --sf-animation-slide-in-right: sf-slide-in-right var(--sf-duration-normal) var(--sf-ease-out) both; --sf-animation-slide-in-up: sf-slide-in-up var(--sf-duration-normal) var(--sf-ease-out) both; --sf-animation-spin: sf-spin var(--sf-duration-slower) linear infinite; --sf-aspect: 16 / 9; --sf-bento-cols-default: 4; --sf-bento-gap: var(--sf-gap); --sf-bento-row-compact: 6rem; --sf-bento-row-default: 10rem; --sf-bento-row-tall: 16rem; --sf-blur: 12px; --sf-body-color: var(--sf-color-text); --sf-body-em-style: italic; --sf-body-font-family: var(--sf-font-body); --sf-body-font-size: var(--sf-text-m); --sf-body-font-weight: var(--sf-font-weight-body); --sf-body-line-height: var(--sf-leading-normal); --sf-body-strong-weight: var(--sf-font-weight-strong); --sf-body-text-wrap: pretty; --sf-border: var(--sf-border-width-1) var(--sf-border-style) var(--sf-color-border); --sf-border-scale: 1; --sf-border-strong: var(--sf-border-width-1) var(--sf-border-style) var(--sf-color-border--strong); --sf-border-style: solid; --sf-border-subtle: var(--sf-border-width-1) var(--sf-border-style) var(--sf-color-border--subtle); --sf-border-width-1: calc(1px * var(--sf-border-scale, 1)); --sf-border-width-2: calc(2px * var(--sf-border-scale, 1)); --sf-border-width-3: calc(3px * var(--sf-border-scale, 1)); --sf-border-width-4: calc(4px * var(--sf-border-scale, 1)); --sf-border-width-hairline: 0.5px; --sf-box-border-color: var(--sf-color-border); --sf-box-border-width: 0; --sf-box-padding: var(--sf-space-m); --sf-breakout-width: var(--sf-container-wide); --sf-button-padding-block: var(--sf-space-xs); --sf-button-padding-inline: var(--sf-space-m); --sf-button-radius: var(--sf-radius-m); --sf-caret-color: var(--sf-color-action); --sf-center-gutter: var(--sf-gutter); --sf-center-max: var(--sf-container-default); --sf-cluster-align: center; --sf-cluster-gap: var(--sf-gap); --sf-cluster-justify: flex-start; --sf-code-font-size: 0.875em; --sf-color-action: var(--sf-color-action-source-light); --sf-color-action--active: var(--sf-color-action-xdark); --sf-color-action--hover: var(--sf-color-action-darker); --sf-color-action-100: color-mix(in oklab, var(--sf-color-action) var(--sf-palette-mix-100), var(--sf-color-surface)); --sf-color-action-200: color-mix(in oklab, var(--sf-color-action) var(--sf-palette-mix-200), var(--sf-color-surface)); --sf-color-action-300: color-mix(in oklab, var(--sf-color-action) var(--sf-palette-mix-300), var(--sf-color-surface)); --sf-color-action-400: color-mix(in oklab, var(--sf-color-action) var(--sf-palette-mix-400), var(--sf-color-surface)); --sf-color-action-50: color-mix(in oklab, var(--sf-color-action) var(--sf-palette-mix-50), var(--sf-color-surface)); --sf-color-action-500: var(--sf-color-action); --sf-color-action-600: color-mix(in oklab, var(--sf-color-action) var(--sf-palette-mix-600), var(--sf-color-text)); --sf-color-action-700: color-mix(in oklab, var(--sf-color-action) var(--sf-palette-mix-700), var(--sf-color-text)); --sf-color-action-800: color-mix(in oklab, var(--sf-color-action) var(--sf-palette-mix-800), var(--sf-color-text)); --sf-color-action-900: color-mix(in oklab, var(--sf-color-action) var(--sf-palette-mix-900), var(--sf-color-text)); --sf-color-action-950: color-mix(in oklab, var(--sf-color-action) var(--sf-palette-mix-950), var(--sf-color-text)); --sf-color-action-a10: oklch(from var(--sf-color-action) l c h / 0.10); --sf-color-action-a30: oklch(from var(--sf-color-action) l c h / 0.30); --sf-color-action-a5: oklch(from var(--sf-color-action) l c h / 0.05); --sf-color-action-a50: oklch(from var(--sf-color-action) l c h / 0.50); --sf-color-action-a80: oklch(from var(--sf-color-action) l c h / 0.80); --sf-color-action-darker: var(--sf-color-action-600); --sf-color-action-ghost: oklch(from var(--sf-color-action) l c h / 0.05); --sf-color-action-lighter: var(--sf-color-action-400); --sf-color-action-muted: oklch(from var(--sf-color-action) l c h / 0.30); --sf-color-action-source-dark: oklch(0.70 0.198 235); --sf-color-action-source-light: oklch(0.50 0.22 235); --sf-color-action-subtle: oklch(from var(--sf-color-action) l c h / 0.10); --sf-color-action-superdark: var(--sf-color-action-950); --sf-color-action-superlight: var(--sf-color-action-50); --sf-color-action-xdark: var(--sf-color-action-800); --sf-color-action-xlight: var(--sf-color-action-200); --sf-color-base: var(--sf-color-base-source-light); --sf-color-base--active: var(--sf-color-base-xdark); --sf-color-base--hover: var(--sf-color-base-darker); --sf-color-base-100: color-mix(in oklab, var(--sf-color-text) var(--sf-palette-mix-100), var(--sf-color-base)); --sf-color-base-200: color-mix(in oklab, var(--sf-color-text) var(--sf-palette-mix-200), var(--sf-color-base)); --sf-color-base-300: color-mix(in oklab, var(--sf-color-text) var(--sf-palette-mix-300), var(--sf-color-base)); --sf-color-base-400: color-mix(in oklab, var(--sf-color-text) var(--sf-palette-mix-400), var(--sf-color-base)); --sf-color-base-50: color-mix(in oklab, var(--sf-color-text) var(--sf-palette-mix-50), var(--sf-color-base)); --sf-color-base-500: var(--sf-color-base); --sf-color-base-600: color-mix(in oklab, var(--sf-color-base) var(--sf-palette-mix-600), var(--sf-color-text)); --sf-color-base-700: color-mix(in oklab, var(--sf-color-base) var(--sf-palette-mix-700), var(--sf-color-text)); --sf-color-base-800: color-mix(in oklab, var(--sf-color-base...
4018:  �[90m �[2m❯�[22m Object.getElementError node_modules/@testing-library/dom/dist/config.js:�[2m37:19�[22m�[39m
4019:  �[90m �[2m❯�[22m node_modules/@testing-library/dom/dist/query-helpers.js:�[2m76:38�[22m�[39m
4020:  �[90m �[2m❯�[22m node_modules/@testing-library/dom/dist/query-helpers.js:�[2m109:15�[22m�[39m
4021:  �[36m �[2m❯�[22m tests-components/studios.test.js:�[2m43:16�[22m�[39m
4022:  �[90m 41| �[39m      �[34mexpect�[39m(�[34mgetByText�[39m(name))�[33m.�[39m�[34mtoBeInTheDocument�[39m()�[33m;�[39m
4023:  �[90m 42| �[39m      �[35mfor�[39m (�[35mconst�[39m text �[35mof�[39m expectedText) {
4024:  �[90m 43| �[39m        �[34mexpect�[39m(�[34mgetAllByText�[39m(text)�[33m.�[39mlength)�[33m.�[39m�[34mtoBeGreaterThan�[39m(�[34m0�[39m)�[33m;�[39m
4025:  �[90m   | �[39m               �[31m^�[39m
4026:  �[90m 44| �[39m      }
4027:  �[90m 45| �[39m    })�[33m;�[39m
4028:  �[31m�[2m⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯�[22m�[39m
4029:  �[2m Test Files �[22m �[1m�[31m1 failed�[39m�[22m�[2m | �[22m�[1m�[32m5 passed�[39m�[22m�[90m (6)�[39m
4030:  �[2m      Tests �[22m �[1m�[31m1 failed�[39m�[22m�[2m | �[22m�[1m�[32m34 passed�[39m�[22m�[90m (35)�[39m
4031:  �[2m   Start at �[22m 09:51:51
4032:  �[2m   Duration �[22m 10.90s�[2m (transform 1.98s, setup 455ms, collect 4.85s, tests 9.84s, environment 3.14s, prepare 647ms)�[22m
4033:  ##[error]TestingLibraryElementError: Unable to find an element with the text: Source pairs. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
4034:  
4035:  Ignored nodes: comments, script, style
4036:  <body>
4037:    <div>
4038:      <section
4039:        class="studio studio--color svelte-10uh662"
4040:        style="color-scheme: light; --sf-is-dark: 0; --sf-alternate-gap: var(--sf-content-gap); --sf-alternate-inner-gap: var(--sf-gap); --sf-animation-blink: sf-blink calc(1s * var(--sf-motion-scale)) steps(1, end) infinite; --sf-animation-color-pulse: sf-color-pulse var(--sf-duration-slow) var(--sf-ease-in-out) infinite; --sf-animation-delay-1: calc(75ms * var(--sf-motion-scale)); --sf-animation-delay-2: calc(150ms * var(--sf-motion-scale)); --sf-animation-delay-3: calc(225ms * var(--sf-motion-scale)); --sf-animation-delay-4: calc(300ms * var(--sf-motion-scale)); --sf-animation-delay-5: calc(375ms * var(--sf-motion-scale)); --sf-animation-fade-in: sf-fade-in var(--sf-duration-normal) var(--sf-ease-out) both; --sf-animation-fade-out: sf-fade-out var(--sf-duration-normal) var(--sf-ease-in) both; --sf-animation-float: sf-float calc(3s * var(--sf-motion-scale)) var(--sf-ease-in-out) infinite; --sf-animation-ping: sf-ping var(--sf-duration-slow) var(--sf-ease-out) infinite; --sf-animation-scale-down: sf-scale-down var(--sf-duration-normal) var(--sf-ease-in) both; --sf-animation-scale-up: sf-scale-up var(--sf-duration-normal) var(--sf-ease-overshoot) both; --sf-animation-shimmer: sf-shimmer calc(1.5s * var(--sf-motion-scale)) var(--sf-ease-in-out) infinite; --sf-animation-slide-in-down: sf-slide-in-down var(--sf-duration-normal) var(--sf-ease-out) both; --sf-animation-slide-in-left: sf-slide-in-left var(--sf-duration-normal) var(--sf-ease-out) both; --sf-animation-slide-in-right: sf-slide-in-right var(--sf-duration-normal) var(--sf-ease-out) both; --sf-animation-slide-in-up: sf-slide-in-up var(--sf-duration-normal) var(--sf-ease-out) both; --sf-animation-spin: sf-spin var(--sf-duration-slower) linear infinite; --sf-aspect: 16 / 9; --sf-bento-cols-default: 4; --sf-bento-gap: var(--sf-gap); --sf-bento-row-compact: 6rem; --sf-bento-row-default: 10rem; --sf-bento-row-tall: 16rem; --sf-blur: 12px; --sf-body-color: var(--sf-color-text); --sf-body-em-style: italic; --sf-body-font-family: var(--sf-font-body); --sf-body-font-size: var(--sf-text-m); --sf-body-font-weight: var(--sf-font-weight-body); --sf-body-line-height: var(--sf-leading-normal); --sf-body-strong-weight: var(--sf-font-weight-strong); --sf-body-text-wrap: pretty; --sf-border: var(--sf-border-width-1) var(--sf-border-style) var(--sf-color-border); --sf-border-scale: 1; --sf-border-strong: var(--sf-border-width-1) var(--sf-border-style) var(--sf-color-border--strong); --sf-border-style: solid; --sf-border-subtle: var(--sf-border-width-1) var(--sf-border-style) var(--sf-color-border--subtle); --sf-border-width-1: calc(1px * var(--sf-border-scale, 1)); --sf-border-width-2: calc(2px * var(--sf-border-scale, 1)); --sf-border-width-3: calc(3px * var(--sf-border-scale, 1)); --sf-border-width-4: calc(4px * var(--sf-border-scale, 1)); --sf-border-width-hairline: 0.5px; --sf-box-border-color: var(--sf-color-border); --sf-box-border-width: 0; --sf-box-padding: var(--sf-space-m); --sf-breakout-width: var(--sf-container-wide); --sf-button-padding-block: var(--sf-space-xs); --sf-button-padding-inline: var(--sf-space-m); --sf-button-radius: var(--sf-radius-m); --sf-caret-color: var(--sf-color-action); --sf-center-gutter: var(--sf-gutter); --sf-center-max: var(--sf-container-default); --sf-cluster-align: center; --sf-cluster-gap: var(--sf-gap); --sf-cluster-justify: flex-start; --sf-code-font-size: 0.875em; --sf-color-action: var(--sf-color-action-source-light); --sf-color-action--active: var(--sf-color-action-xdark); --sf-color-action--hover: var(--sf-color-action-darker); --sf-color-action-100: color-mix(in oklab, var(--sf-color-action) var(--sf-palette-mix-100), var(--sf-color-surface)); --sf-color-action-200: color-mix(in o
4041:  ##[error]Process completed with exit code 1.
4042:  Post job cleanup.

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 5 rules

Grey Divider


Action required

1. ShadeRamp stale on colorKey 🐞 Bug ≡ Correctness
Description
ShadeRamp.svelte now derives ALL_SHADE_TOKENS from the new colorKey prop, but the measuring
$effect does not establish a reactive dependency on that derived value because it’s only read
inside queueMicrotask. As a result, changing colorKey can leave resolved computed for the
previous token set and the UI can stay in the “resolving…” state for the newly selected family until
some other dependency changes.
Code

configurator/src/components/ShadeRamp.svelte[R18-24]

+  let { colorKey = null, showIntro = true } = $props();
+
  const BRAND_KEYS = BRAND_COLOR_KEYS.filter((k) => k.group === 'brand');
+  const visibleBrandKeys = $derived(colorKey ? BRAND_KEYS.filter((k) => k.key === colorKey) : BRAND_KEYS);

  const SHADE_STEPS = [
    { suffix: '-superlight', label: 'superlight' },
Relevance

⭐⭐⭐ High

Team previously accepted fixing $effect deps with queueMicrotask measurement to avoid stale
resolution (ColorAssignments.svelte).

PR-#369
PR-#398

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The token list is now derived from colorKey, but the measuring effect does not reference it during
effect evaluation, so it won’t rerun when colorKey changes.

configurator/src/components/ShadeRamp.svelte[18-35]
configurator/src/components/ShadeRamp.svelte[40-55]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`ALL_SHADE_TOKENS` is now `$derived(...)` from `colorKey`, but the `$effect` that measures swatches never reads `ALL_SHADE_TOKENS` synchronously, so changes to `colorKey` won’t retrigger the measuring work.

### Issue Context
The effect schedules work with `queueMicrotask`, and the only read of `ALL_SHADE_TOKENS` happens inside that callback, which does not participate in Svelte’s dependency tracking.

### Fix Focus Areas
- configurator/src/components/ShadeRamp.svelte[18-56]

### Suggested fix
Inside the `$effect`, read `ALL_SHADE_TOKENS` before scheduling the microtask (e.g. `const tokens = ALL_SHADE_TOKENS;`) and iterate `tokens` inside the microtask. This both (a) establishes reactivity on `colorKey`/`visibleBrandKeys` changes and (b) measures a stable snapshot of the token list for that effect run.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

2. Duplicated preview style blocks 🐞 Bug ➹ Performance
Description
ColorStudio.svelte generates stageStyle by serializing *all* tokens into a CSS declarations
string and then applies it to every gradient swatch element. This duplicates a very large inline
style attribute N times and increases DOM size and style recalculation work on every
overrides/theme update.
Code

configurator/src/components/editors/ColorStudio.svelte[R126-132]

+    {:else if activePanel === 'gradients'}
+      <section class="gradient-grid">
+        {#each gradientBuilder.tokens.filter(exists) as name (name)}
+          <article class="gradient-card">
+            <div class="gradient-card__swatch" style={stageStyle} style:background={gradientValue(name)}></div>
+            <div class="gradient-card__body"><code>{name}</code><div class="gradient-card__buttons">{#each gradientAngles as angle (angle)}<button type="button" class="cfg-btn cfg-btn--ghost cfg-btn--sm" onclick={() => setGradient(name, angle, gradientStops[0][0], gradientStops[0][1])}>{angle}°</button>{/each}</div><div class="gradient-card__buttons">{#each gradientStops as stops, i (i)}<button type="button" class="cfg-btn cfg-btn--sm" onclick={() => setGradient(name, 135, stops[0], stops[1])}>Preset {i + 1}</button>{/each}</div><TokenRow token={token(name)} label="Raw gradient" help="Power-user CSS value: linear/radial/conic, stops, color-mix(), vars." showRawInfo forceEditable /></div>
+          </article>
Relevance

⭐ Low

Similar request to avoid duplicated buildPreviewDeclarations/stageStyle inline styles was explicitly
rejected by reviewers.

PR-#402

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
stageStyle is built from a full-token serialization and is applied per gradient swatch, causing
repeated large inline styles.

configurator/src/components/editors/ColorStudio.svelte[39-43]
configurator/src/components/editors/ColorStudio.svelte[126-133]
configurator/src/lib/preview.js[28-46]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The gradients panel repeats `style={stageStyle}` (a large declaration block containing every token) on each swatch, duplicating the same huge inline style string across the DOM.

### Issue Context
`buildPreviewDeclarations` iterates `allTokens` and `overrides` to build a full declaration list, so the resulting string is large.

### Fix Focus Areas
- configurator/src/components/editors/ColorStudio.svelte[39-43]
- configurator/src/components/editors/ColorStudio.svelte[126-133]
- configurator/src/lib/preview.js[28-46]

### Suggested fix
Move `style={stageStyle}` to a single parent element (e.g. the `<section class="gradient-grid">` or an outer wrapper) so all gradient swatches inherit the seeded custom properties, and keep per-swatch `style:background={...}` only on the individual swatch nodes.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +18 to 24
let { colorKey = null, showIntro = true } = $props();

const BRAND_KEYS = BRAND_COLOR_KEYS.filter((k) => k.group === 'brand');
const visibleBrandKeys = $derived(colorKey ? BRAND_KEYS.filter((k) => k.key === colorKey) : BRAND_KEYS);

const SHADE_STEPS = [
{ suffix: '-superlight', label: 'superlight' },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Shaderamp stale on colorkey 🐞 Bug ≡ Correctness

ShadeRamp.svelte now derives ALL_SHADE_TOKENS from the new colorKey prop, but the measuring
$effect does not establish a reactive dependency on that derived value because it’s only read
inside queueMicrotask. As a result, changing colorKey can leave resolved computed for the
previous token set and the UI can stay in the “resolving…” state for the newly selected family until
some other dependency changes.
Agent Prompt
### Issue description
`ALL_SHADE_TOKENS` is now `$derived(...)` from `colorKey`, but the `$effect` that measures swatches never reads `ALL_SHADE_TOKENS` synchronously, so changes to `colorKey` won’t retrigger the measuring work.

### Issue Context
The effect schedules work with `queueMicrotask`, and the only read of `ALL_SHADE_TOKENS` happens inside that callback, which does not participate in Svelte’s dependency tracking.

### Fix Focus Areas
- configurator/src/components/ShadeRamp.svelte[18-56]

### Suggested fix
Inside the `$effect`, read `ALL_SHADE_TOKENS` before scheduling the microtask (e.g. `const tokens = ALL_SHADE_TOKENS;`) and iterate `tokens` inside the microtask. This both (a) establishes reactivity on `colorKey`/`visibleBrandKeys` changes and (b) measures a stable snapshot of the token list for that effect run.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant